/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package percussiongenerator.gui.view;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.ResourceBundle;
import org.jfonia.connect5.basics.BasicValueNode;
import org.jfonia.connect5.basics.MutableValueNode;
import org.jfonia.connect5.intervals.Interval;
import org.jfonia.constants.ModelConstants;
import org.jfonia.model.PerformanceNode;
import org.jfonia.model.PerformanceSequence;
import org.jfonia.model.TempoNode;
import org.jfonia.model.ToneSequence;
import org.jfonia.model.elements.TimeSignature;
import org.jfonia.model.elements.Tone;
import percussiongenerator.commands.ChangeBegin;
import percussiongenerator.commands.ChangeEnd;
import percussiongenerator.commands.ChangePitchAndVelocity;
import percussiongenerator.commands.CommandHistory;
import percussiongenerator.commands.SplitMultipleTones;
import percussiongenerator.model.Pair;
import percussiongenerator.model.Track;

/**
 *
 * @author Jannes Plyson
 */
public class TimeLineView extends View implements MouseMotionListener{
    private Color foregroundColor;    
    private Rectangle selectionRectangle;
    private static boolean scroll;
    private Grid grid;
    private ArrayList<MutableValueNode<Color>> gridColors;
    private MutableValueNode<Long> timeNode;
    public TimeLineView(Track track,MutableValueNode<View> selectedView,MutableValueNode<ArrayList<Integer>> selectedInterval){
        super(track,selectedView,selectedInterval);
        timeNode = new BasicValueNode<Long>(0l);
        gridColors = new ArrayList<MutableValueNode<Color>>();        
        gridColors.add(new BasicValueNode<Color>(new Color(100,100,100)));
        gridColors.add(new BasicValueNode<Color>(Color.BLACK));
        updateGrid();
        backgroundColor = Color.WHITE;
        foregroundColor =  Color.BLACK;
        this.setFocusable(true);
        this.addMouseMotionListener(this);
        viewOptions = new TimeLineViewOptions(this);
        heightNode.setValue(50);               
        setWidthNode();
        scroll = false;
        TimeSignature ts = (TimeSignature)track.getStaff().getSequence(TimeSignature.class).getIntervalFromIndex(0);
        ts.getNominatorNode().addObserver(this);
        ts.getDenominatorNode().addObserver(this);        
    }

    public MutableValueNode<Long> getTimeNode(){
        return timeNode;
    }

    public ArrayList<MutableValueNode<Color>> getGridColors(){
        return gridColors;
    }

    public Grid getGrid(){
        return grid;
    }

    public static void setScroll(boolean scroll_){
        scroll = scroll_;
    }
    public static boolean getScroll(){
        return scroll;
    }

    @Override
    public void paintView(Graphics g) {                
        g.setColor(Color.BLACK);
        g.drawRect(0, 0, getWidth(), getHeight()-1);
        int timeOffset = (int)(track.getTime()*getWidth()/track.getPerformanceLength());
        if(scroll){
            paintGrid(g,timeOffset);
            paintTrack(g,timeOffset);            
        }else{
            paintGrid(g,0);
            paintTrack(g,0);
            paintTimePointer(g,timeOffset);            
        }        
        paintSelectionView(g);
    }

    private void paintGrid(Graphics g, int timeOffset){        
        grid.paintGrid(g);
    }

    private long getBeatWidth(){
        int bpm = ((TempoNode)track.getStaff().getTempoMap().getIntervalFromIndex(0)).getBPM();
        return (60l*ModelConstants.PERFORMANCE_TO_SECOND)/bpm;

    }

    private long getMeasureWidth(){
        TimeSignature ts = (TimeSignature)track.getStaff().getSequence(TimeSignature.class).getIntervalFromIndex(0);        
        return getBeatWidth()*ts.getNominator();
    }

    private void paintSelectionView(Graphics g){
        if(selectionRectangle != null){
            g.setColor(Color.BLUE);
            g.drawRect(selectionRectangle.x, selectionRectangle.y, selectionRectangle.width, selectionRectangle.height);
        }
    }

    private void paintTimePointer(Graphics g, int timeOffset){
        g.setColor(Color.BLUE);
        g.fillRect(timeOffset, 0, 2, getHeight());
    }

    private void paintTrack(Graphics g,int timeOffset){
        g.setColor(foregroundColor);        
        long trackLength = track.getPerformanceLength();
        int yOffset = getHeight()/4;
        int halfHeight = getHeight()/2;
        ToneSequence sequence = track.getStaff().getToneSequence();
        if(sequence != null){            
            g.fillRect(getWidth()-timeOffset-1, 0, 2, getHeight());
            for(int i = 0; i < sequence.size(); i++){
                PerformanceNode node = sequence.getPerformanceNodeFromIndex(i);
                if(selectedInterval.getValue() != null && selectedInterval.getValue().contains(i)){
                    g.setColor(selectedForegroundColor);
                }else if(node.getPitch() != null || node.getVelocity() != 0){
                    g.setColor(foregroundColor);
                }else{
                    g.setColor(Color.GRAY);
                }
                int xOffset = (int)(node.getBegin() * getWidth() / trackLength) - timeOffset;
                int width = (int)(node.getDifference() * getWidth() / trackLength);
                if(xOffset < 0){
                    g.fillRect(getWidth()+xOffset, yOffset, 2, halfHeight);
                    if(xOffset + width > 0){//leftOver from line at the beginning;
                        g.fillRect(0, halfHeight-1, xOffset+width, 2);//at the beginning
                        g.fillRect(getWidth()+xOffset, halfHeight-1, -xOffset,2);//at the end
                    }else{
                        g.fillRect(getWidth()+xOffset, halfHeight-1, width,2);//at the end
                    }
                }else{
                    g.fillRect(xOffset, yOffset, 2, halfHeight);
                    g.fillRect(xOffset,halfHeight-1,width,2);
                }
            }
        }        
    }
        
    @Override
    public void keyPressed(KeyEvent e) {
        super.keyPressed(e);
        if(e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_LEFT){
            if(selectedInterval.getValue() != null){
                if(selectedInterval.getValue().size() > 0){
                    PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
                    long direction = 1;
                    if(KeyEvent.VK_LEFT == e.getKeyCode()){
                        direction = -1;
                    }
                    for(Integer index: selectedInterval.getValue()){
                        PerformanceNode perNode = ((PerformanceNode)(perSeq.getIntervalFromIndex(index)));
                        CommandHistory.addCommandAndExecute(new ChangeBegin(perNode, 1000000l*direction));
                    }
                }
            }else if (e.getKeyCode() == KeyEvent.VK_RIGHT){
                selectedInterval.setValue(null);
                track.getStaff().getToneSequence().shift(1000000l);
            }
        }else if(e.getKeyCode() == KeyEvent.VK_END){
            long measureWidth = getMeasureWidth();
            PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
            if(perSeq.getDifference() % measureWidth != 0){
                long diff = measureWidth - perSeq.getDifference() % measureWidth;
                PerformanceNode perNode = (PerformanceNode)perSeq.getIntervalFromIndex(perSeq.size()-1);
                perNode.getEndNode().setValue(perNode.getEnd()+diff);
            }
        }else if(e.getKeyCode() == KeyEvent.VK_S){
            if(selectedInterval.getValue() != null){
                ArrayList<Integer> list = selectedInterval.getValue();
                ArrayList<PerformanceNode> perNodes = new ArrayList<PerformanceNode>();
                PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
                for(Integer index: list){
                    perNodes.add((PerformanceNode)(perSeq.getIntervalFromIndex(index)));
                }
                CommandHistory.addCommandAndExecute(new SplitMultipleTones(perNodes, perSeq));
            }
        }else if(e.getKeyCode() == KeyEvent.VK_M){
            if(selectedInterval.getValue() != null && selectedInterval.getValue().size() > 0){
                ArrayList<Integer> list = selectedInterval.getValue();
                PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
                for(Integer index: list){
                    PerformanceNode perNode = (PerformanceNode)(perSeq.getIntervalFromIndex(index));
                    if(perNode.getPitch() != null){
                        CommandHistory.addCommandAndExecute(new ChangePitchAndVelocity(perNode, null, 0));
                    }else{
                        CommandHistory.addCommandAndExecute(new ChangePitchAndVelocity(perNode, 50, 120));
                    }
                }
            }else{
                if(track.isMuted()){
                    track.setMute(false);
                }else{
                    track.setMute(true);
                }
            }
        }else if(e.getKeyCode() == KeyEvent.VK_A){
            PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
            PerformanceNode perNode = (PerformanceNode)perSeq.getIntervalFromIndex(perSeq.size()-1);            
            CommandHistory.addCommandAndExecute(new ChangeEnd(perNode, getMeasureWidth()));
        }
        repaint();
    }   
    
    public void updateLanguage(ResourceBundle labels) {
    }
        
    private void setWidthNode(){
        int w = (int)(track.getPerformanceLength()*zoom.getValue()/(ModelConstants.PERFORMANCE_TO_SECOND*100));        
        updateGrid();
        widthNode.setValue(w);
    }

    public void updateGrid(){        
        if(grid == null){            
            grid = new Grid(gridColors);
        }
        ArrayList<Long> measureWidth = new ArrayList<Long>();
        measureWidth.add(getBeatWidth());
        measureWidth.add(getMeasureWidth());
        grid.setMeasure(measureWidth);
    }

    public void intervalAdded(Interval interval) {
        setWidthNode();
    }

    public void intervalRemoved(Interval interval, int index) {
        setWidthNode();
    }

    public void intervalEndChanged(Interval interval) {
        setWidthNode();
    }

    private void selectIntervals(int start, int end){
        ArrayList<Integer> list = null;
        if(ctrlPressed && selectedInterval.getValue() != null){
            list = selectedInterval.getValue();
        }else{
            list = new ArrayList<Integer>();
        }
        for(int index = start; index <= end; index++){
            if(!list.contains(index)){
                list.add(index);
            }
        }
        selectedInterval.setValue(list);
        selectedInterval.notifyObservers();
    }

    @Override
    public void mousePressedExtra(MouseEvent e){        
        if(e.getButton() == MouseEvent.BUTTON1 && e.getY() > getHeight()*0.2 && e.getY()< getHeight() * 0.8 ){
            long trackLength = track.getPerformanceLength();            
            long calculatedTime = (e.getX())*trackLength/getWidth();
            PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
            if(calculatedTime > trackLength){
                calculatedTime = trackLength;
            }
            int index = perSeq.indexOf(perSeq.getIntervalFromValue(calculatedTime));
            selectIntervals(index, index);
        }else if(e.getButton() == MouseEvent.BUTTON1){
            selectedInterval.setValue(null);
        }        
        else if(e.getButton() == MouseEvent.BUTTON3 && selection == null && selectedInterval != null){
            try{
                ArrayList<PerformanceNode> list = new ArrayList<PerformanceNode>();
                PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
                for(int i : selectedInterval.getValue()){
                    list.add(((PerformanceNode)perSeq.getIntervalFromIndex(i)).clone());
                }
                selection = new Pair<String,Object>("performanceIntervals",list);                
            }catch(Exception exc){                
                selectedInterval.setValue(null);
            }
        }
    }

    

    @Override
    public void mouseReleasedExtra(MouseEvent e){
        selectionRectangle = null;
        if(e.getButton() == MouseEvent.BUTTON1 && Math.abs(mousePressedStart.first.x - mousePressedEnd.first.x) > 10 ){
            long trackLength = track.getPerformanceLength();
            long timeOffset = track.getTime()*getWidth()/trackLength;
            long minX = Math.min(mousePressedStart.first.x, mousePressedEnd.first.x);
            long maxX = Math.max(mousePressedStart.first.x, mousePressedEnd.first.x);
            long calculatedTimeStart = (minX+timeOffset)*trackLength/getWidth();
            if(calculatedTimeStart < 0 ){
                calculatedTimeStart = 0;
            }
            long calculatedTimeEnd = (maxX+timeOffset)*trackLength/getWidth();
            if(calculatedTimeEnd > trackLength-1){
                calculatedTimeEnd = trackLength -1;
            }
            PerformanceSequence perSeq = track.getStaff().getToneSequence().getPerformanceSequence();
            int indexStart = perSeq.indexOf(perSeq.getIntervalFromValue(calculatedTimeStart));
            int indexEnd = perSeq.indexOf(perSeq.getIntervalFromValue(calculatedTimeEnd));
            selectIntervals(indexStart, indexEnd);
        }else if(e.getButton() == MouseEvent.BUTTON3){
            if(selection != null && selection.first.equals("performanceIntervals")){
                ArrayList<Integer> list = selectedInterval.getValue();
                if(list != null && list.size() > 1){                
                    PerformanceSequence perSeq = mouseEnteredView.track.getStaff().getToneSequence().getPerformanceSequence();
                    ArrayList<PerformanceNode> nodeList = (ArrayList<PerformanceNode>)selection.second;
                    long trackLength = track.getPerformanceLength();
                    long timeOffset = track.getTime()*getWidth()/trackLength;
                    long x = mousePressedEnd.first.x;
                    long calculatedTime = (e.getX()+timeOffset)*trackLength/getWidth();
                    int index = perSeq.indexOf(perSeq.getIntervalFromValue(calculatedTime));
                    index++;
                    ArrayList<Integer> selectedIntervals = new ArrayList<Integer>();
                    if(index > perSeq.size()){
                        index = perSeq.size();
                    }
                    for(PerformanceNode perNode : nodeList){
                        perSeq.insert(index,perNode);
                        selectedIntervals.add(index);
                        index++;
                    }
                    mouseEnteredView.selectedInterval.setValue(selectedIntervals);
                }else if(list != null && list.size() == 1){
                    PerformanceSequence perSeq = mouseEnteredView.track.getStaff().getToneSequence().getPerformanceSequence();
                    PerformanceNode node = (PerformanceNode)perSeq.getIntervalFromIndex(list.get(0));
                    long trackLength = track.getPerformanceLength();
                    long timeOffset = 0l;
                    if(scroll){
                        timeOffset = track.getTime()*getWidth()/trackLength;
                    }
                    long calculatedTime = (e.getX()+timeOffset)*trackLength/getWidth();
                    if(e.isControlDown()){
                        calculatedTime = grid.getClosestGridLine(calculatedTime);
                    }
                    if(e.isShiftDown()){
                        if(calculatedTime > node.getBegin()){
                            if(list.get(0)<perSeq.size()-1){
                                PerformanceNode nextNode = (PerformanceNode)perSeq.getIntervalFromIndex(list.get(0)+1);
                                if(calculatedTime < nextNode.getEnd()){
                                    CommandHistory.addCommandAndExecute(new ChangeEnd(node, calculatedTime - node.getEnd()));
                                }
                            }
                        }
                    }else{
                        if(calculatedTime < node.getEnd()){
                            if(list.get(0)>0){
                                PerformanceNode prevNode = (PerformanceNode)perSeq.getIntervalFromIndex(list.get(0)-1);
                                if(calculatedTime > prevNode.getBegin()){
                                    track.getStaff().getToneSequence().setLockNotation(true);
                                    CommandHistory.addCommandAndExecute(new ChangeBegin(node, calculatedTime - node.getBegin()));
                                    track.getStaff().getToneSequence().setLockNotation(false);
                                }
                            }
                        }
                    }
                    repaint();

                }
            }
        }
        selection = null;
    }

    public void mouseDragged(MouseEvent e) {        
        if(mouseButtonPressed == MouseEvent.BUTTON1){
            try{
                int x = Math.min(mousePressedStart.first.x, e.getX());
                int y = Math.min(mousePressedStart.first.y, e.getY());
                int width = Math.abs(mousePressedStart.first.x - e.getX());
                int height = Math.abs(mousePressedStart.first.y - e.getY());
                selectionRectangle = new Rectangle(x,y,width,height);
                repaint();
            }catch(Exception exc){}
        }        
    }

    public void mouseMoved(MouseEvent e) {       
        if(selectionRectangle != null){
            selectionRectangle = null;
            repaint();
        }
        long trackLength = track.getPerformanceLength();
        long timeOffset = e.getX()*track.getPerformanceLength()/getWidth();        
        timeNode.setValue(timeOffset);        
    }

    public void zoomUpdated(){
        setWidthNode();     
    }

    public Color getColor(int level){
        int grayValue = level * 85;
        return new Color(grayValue,grayValue,grayValue);
    }

     public void onNotify(Object source){
        super.onNotify(source);
        TimeSignature ts = (TimeSignature)track.getStaff().getSequence(TimeSignature.class).getIntervalFromIndex(0);
        if(source == tempoNode.getBPMNode() || source == ts.getNominatorNode() || source == ts.getDenominatorNode()){
            updateGrid();
            repaint();            
        }        
    }

    protected class Grid{
        ArrayList<Long> measureWidth;
        ArrayList<Long> gridWidth;
        ArrayList<MutableValueNode<Color>> gridColor;

        public Grid(ArrayList<MutableValueNode<Color>> colors){
            measureWidth = new ArrayList<Long>();
            gridWidth = new ArrayList<Long>();
            gridColor = colors;
        }

        public void setMeasure(ArrayList<Long> measureWidth){
            this.measureWidth = measureWidth;
        }

        public void add(long width){
            gridWidth.add(width);
            Collections.sort(gridWidth);
            if(gridWidth.size()+measureWidth.size() > gridColor.size()){                
                gridColor.add(new BasicValueNode<Color>(Color.BLUE));
            }
        }        

        public long getClosestGridLine(long time){
            long smallestDistance = Long.MAX_VALUE;
            for(int i=0; i < grid.measureWidth.size(); i++){
                long distance = time%grid.measureWidth.get(i);
                if(distance > grid.measureWidth.get(i)/2){
                    distance -= grid.measureWidth.get(i);
                }
                if(Math.abs(distance) < Math.abs(smallestDistance)){
                    smallestDistance = distance;
                }
            }
            for(int i=0; i < grid.gridWidth.size(); i++){
                long distance = time%grid.gridWidth.get(i);
                if(distance > grid.gridWidth.get(i)/2){
                    distance -= grid.gridWidth.get(i);
                }
                if(Math.abs(distance) < Math.abs(smallestDistance)){
                    smallestDistance = distance;
                }
            }
            return time - smallestDistance;
        }

        public void paintGrid(Graphics g){
            long trackLength = track.getPerformanceLength();            
            for(int i = 0; i < gridWidth.size(); i++){
                g.setColor(gridColor.get(i+measureWidth.size()).getValue());
                int x = 0;
                int counter = 0;
                long width = gridWidth.get(i);
                while(x < getWidth()){                    
                    g.drawLine(x, 0, x, getHeight());
                    counter++;
                    x = (int)((counter*width*getWidth())/trackLength);
                }
            }
            for(int i = 0; i < measureWidth.size(); i++){
                g.setColor(gridColor.get(i).getValue());
                int x = 0;
                int counter = 0;
                long width = measureWidth.get(i);
                while(x < getWidth()){
                    g.drawLine(x, 0, x, getHeight());
                    counter++;
                    x = (int)((counter*width*getWidth())/trackLength);
                }
            }
        }

    }   
}
